package io.github.flypiggy.stack.operate.log.spring.boot.starter.configuration.advisor;

import io.github.flypiggy.stack.operate.log.spring.boot.starter.aop.LogAspect;
import io.github.flypiggy.stack.operate.log.spring.boot.starter.configuration.EnableAdvisorConfiguration;
import io.github.flypiggy.stack.operate.log.spring.boot.starter.configuration.LogAspectBase;
import io.github.flypiggy.stack.operate.log.spring.boot.starter.context.OperatorGetter;
import io.github.flypiggy.stack.operate.log.spring.boot.starter.datasource.impl.JdbcRepository;
import io.github.flypiggy.stack.operate.log.spring.boot.starter.exception.OperateLogException;
import io.github.flypiggy.stack.operate.log.spring.boot.starter.properties.Jdbc;
import io.github.flypiggy.stack.operate.log.spring.boot.starter.properties.OperateLogProperties;
import io.github.flypiggy.stack.operate.log.spring.boot.starter.properties.enums.DateSuffixEnum;
import io.github.flypiggy.stack.operate.log.spring.boot.starter.utils.DateTimeUtils;
import io.github.flypiggy.stack.operate.log.spring.boot.starter.utils.JdbcUrlUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.util.StringUtils;

import java.time.LocalDateTime;
import java.util.Objects;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

import static io.github.flypiggy.stack.operate.log.spring.boot.starter.properties.Jdbc.DATABASE_NAME;
import static io.github.flypiggy.stack.operate.log.spring.boot.starter.properties.Jdbc.TABLE_NAME;

@Configuration
@ConditionalOnBean(name = "enableJdbc")
@AutoConfigureAfter(EnableAdvisorConfiguration.class)
public class JdbcLogAspectConfiguration extends LogAspectBase {

    private final Logger log = LoggerFactory.getLogger(JdbcLogAspectConfiguration.class);

    private final DataSourceProperties dataSourceProperties;

    public JdbcLogAspectConfiguration(OperatorGetter operatorGetter,
                                      OperateLogProperties operateLogProperties,
                                      DataSourceProperties dataSourceProperties) {
        super(operatorGetter, operateLogProperties);
        this.dataSourceProperties = dataSourceProperties;
    }

    @Bean
    public LogAspect jdbcConfigurableLogAspect() {
        JdbcTemplate jdbcTemplate = this.getJdbcTemplate();
        Jdbc jdbc = operateLogProperties.getJdbc();
        this.checkJdbcProperties(jdbc);
        this.checkDatabaseExist(jdbcTemplate, jdbc);
        this.createTables(jdbcTemplate, jdbc);
        return super.logAspectInitializer(new JdbcRepository(jdbcTemplate, jdbc));
    }

    private JdbcTemplate getJdbcTemplate() throws OperateLogException {
        DataSourceBuilder<?> dataSourceBuilder = dataSourceProperties.initializeDataSourceBuilder();
        log.info("OPERATE-LOG Create JDBC connection for operation log...");
        return new JdbcTemplate(dataSourceBuilder.build());
    }

    private void checkJdbcProperties(Jdbc jdbc) {
        if (!jdbc.getCreateTableSql().contains("%s")) {
            throw new OperateLogException("Error create table sql! " +
                    "Please configure the correct properties of 'spring.operate-log.jdbc.create-table-sql'! " +
                    "The attribute must have a replacement symbol '%s' to replace the real table name.");
        }
        if (!jdbc.getInsertSql().contains("%s")) {
            throw new OperateLogException("Error insert log sql! " +
                    "Please configure the correct properties of 'spring.operate-log.jdbc.insert-sql'! " +
                    "The attribute must have a replacement symbol '%s' to replace the real table name.");
        }
    }

    private void createTables(JdbcTemplate jdbcTemplate, Jdbc jdbc) {
        DateSuffixEnum dateSuffixEnum;
        if (Objects.isNull(dateSuffixEnum = jdbc.getSuffix())) {
            this.checkAndCreateTable(jdbcTemplate, jdbc, jdbc.getTableName());
            return;
        }
        // By default, one table is generated per month
        Supplier<LocalDateTime> nextCycleDateTimeSupplier = () -> LocalDateTime.now().plusMonths(1);
        if (dateSuffixEnum.equals(DateSuffixEnum.DAY)) {
            nextCycleDateTimeSupplier = () -> LocalDateTime.now().plusDays(1);
        } else if (dateSuffixEnum.equals(DateSuffixEnum.YEAR)) {
            nextCycleDateTimeSupplier = () -> LocalDateTime.now().plusYears(1);
        }
        createNextCycleTable(DateSuffixEnum.getJdbcSuffixPattern(jdbc.getSuffix()), nextCycleDateTimeSupplier, jdbcTemplate, jdbc);
    }

    private void createNextCycleTable(String pattern,
                                      Supplier<LocalDateTime> nextCycleDateTimeSupplier,
                                      JdbcTemplate jdbcTemplate,
                                      Jdbc jdbc) {
        ScheduledExecutorService scheduledService = Executors.newScheduledThreadPool(3);
        String tableName = jdbc.getTableName();
        this.checkAndCreateTable(jdbcTemplate, jdbc, tableName + DateTimeUtils.format(LocalDateTime.now(), pattern));
        // Enable a scheduled task to check and create tables
        scheduledService.scheduleAtFixedRate(() -> {
            String nextCycleSuffixStr = DateTimeUtils.format(nextCycleDateTimeSupplier.get(), pattern);
            log.info("OPERATE-LOG The system starts to check the table of {} for creating the next time period.", tableName + nextCycleSuffixStr);
            this.checkAndCreateTable(jdbcTemplate, jdbc, tableName + nextCycleSuffixStr);
        }, 0, 1, TimeUnit.DAYS);
    }

    private void checkAndCreateTable(JdbcTemplate jdbcTemplate, Jdbc jdbc, String tableName) throws OperateLogException {
        String databaseName = this.getActualDatabaseName(jdbc.getDatabaseName());
        String checkTableSql = jdbc.getCheckTableSql();
        checkTableSql = checkTableSql
                .replace(TABLE_NAME, tableName)
                .replace(DATABASE_NAME, databaseName);
        Long value = jdbcTemplate.queryForObject(checkTableSql, Long.class);
        if (value != null && value > 0) {
            log.info("OPERATE-LOG There is already an operation log table. There is no need to create a new table! Table's name is {}.", tableName);
            return;
        }
        log.info("OPERATE-LOG The operation log table does not exist yet. We are about to create a new table! Table's name is {}.", tableName);
        String createTableSql = String.format(jdbc.getCreateTableSql(), tableName);
        jdbcTemplate.update(createTableSql);
    }

    private String getActualDatabaseName(String databaseName) {
        if (!StringUtils.hasText(databaseName)) {
            databaseName = JdbcUrlUtils.findDatabaseName(dataSourceProperties.getUrl());
        }
        return databaseName;
    }

    private void checkDatabaseExist(JdbcTemplate jdbcTemplate, Jdbc jdbc) {
        log.info("OPERATE-LOG Initialization checks whether the database exists.");
        try {
            Integer result = jdbcTemplate.queryForObject(jdbc.getCheckDatabaseSql(), Integer.class);
            if (result == null || 1 != result) {
                throw new OperateLogException();
            }
        } catch (DataAccessException e) {
            throw new OperateLogException("Error database not found! Please configure the correct database connection!");
        }
    }

}
